package dal

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	"gitee.com/zhangxinzx/wrap-error/pkg/dao"
	"gitee.com/zhangxinzx/wrap-error/pkg/model"
	xerrors "github.com/pkg/errors"
)

var (
	ErrInvalidUserObject     = errors.New("dal: invalid user object")
	ErrInvalidUserMatchRange = errors.New("dal: invalid match range")
	ErrUserDeleted           = errors.New("dal: user has beed deleted")
)

type userDAO struct {
	DB  *sql.DB
	Gen GenerateUUID
}

func NewUserDAO(db *sql.DB, gen GenerateUUID) dao.DAO {
	return &userDAO{DB: db, Gen: gen}
}

func (udao *userDAO) Create(ctx context.Context, obj interface{}) (dao.Entity, error) {
	user, ok := obj.(*model.User)
	if !ok {
		return nil, ErrInvalidUserObject
	}

	now := time.Now()
	e := &userEntity{
		entity{Uid: udao.Gen(), CreatedAt: now, UpdatedAt: now},
		*user,
	}
	const stmt = `INSERT INTO user (uuid, first_name, last_name, age, gender, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
	if _, err := udao.DB.ExecContext(ctx, stmt, e.Uid, e.val.FirstName, e.val.LastName, e.val.Age, e.val.Gender, e.CreatedAt, e.UpdatedAt); err != nil {
		return nil, xerrors.Wrap(err, "could not create user")
	}
	return e, nil
}
func (udao *userDAO) GetByID(ctx context.Context, id string) (dao.Entity, error) {
	const stmt = `SELECT uuid, first_name, last_name, age, gender, created_at, updated_at, deleted_at FROM user WHERE uuid = ?`
	row := udao.DB.QueryRowContext(ctx, stmt, id)

	e := &userEntity{}
	err := row.Scan(&e.Uid, &e.val.FirstName, &e.val.LastName, &e.val.Age, &e.val.Gender, &e.CreatedAt, &e.UpdatedAt, &e.DeletedAt)
	switch {
	case errors.Is(err, sql.ErrNoRows):
		return nil, xerrors.Wrapf(err, "uuid [ %s ] no such user", id)
	case err != nil:
		return nil, err
	case e.DeletedAt.Valid:
		return nil, xerrors.Wrapf(ErrUserDeleted, "uuid [ %s ]", e.Uid)
	default:
		return e, nil
	}
}

func (udao *userDAO) GetMatch(ctx context.Context, filter string, limit [2]int) ([]dao.Entity, error) {
	limitStr := ""
	switch {
	case limit[0] > limit[1]:
		return nil, xerrors.Wrapf(ErrInvalidUserMatchRange, " range (%d, %d)", limit[0], limit[1])
	case limit[0] < 0:
		return nil, xerrors.Wrapf(ErrInvalidUserMatchRange, " range (%d, %d)", limit[0], limit[1])
	case limit[1] != 0:
		limitStr = fmt.Sprintf(" LIMIT %d,%d", limit[0], limit[1])
	default:
		limitStr = " LIMIT 100"
	}

	stmt := `SELECT uuid, first_name, last_name, age, gender, created_at, updated_at, deleted_at FROM user`
	if len(filter) > 0 {
		stmt = stmt + " " + filter
	}
	stmt = stmt + limitStr
	rows, err := udao.DB.QueryContext(ctx, stmt)
	switch {
	case errors.Is(err, sql.ErrNoRows):
		return nil, xerrors.Wrapf(err, "no user present on filter[ %s ] %s", filter, limitStr)
	case err != nil:
		return nil, err
	default:
	}
	defer rows.Close()

	entities := []dao.Entity{}
	for rows.Next() {
		e := &userEntity{}
		deletedAt := sql.NullTime{}
		if err := rows.Scan(&e.Uid, &e.val.FirstName, &e.val.LastName, &e.val.Age, &e.val.Gender, &e.CreatedAt, &e.UpdatedAt, &deletedAt); err != nil {
			return nil, err
		}
		if !deletedAt.Valid {
			entities = append(entities, e)
		}
	}

	return entities, nil
}

func (udao *userDAO) Update(ctx context.Context, id string, obj interface{}) (dao.Entity, error) {
	user, ok := obj.(*model.User)
	if !ok {
		return nil, ErrInvalidUserObject
	}
	entity, err := udao.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}
	e := entity.(*userEntity)
	e.UpdatedAt = time.Now()
	e.val = *user

	const stmt = `UPDATE user SET first_name = ?, last_name = ?, age = ?, gender = ?, updated_at = ? WHERE uuid = ?`
	if _, err := udao.DB.ExecContext(ctx, stmt, e.val.FirstName, e.val.LastName, e.val.Age, e.val.Gender, time.Now(), id); err != nil {
		return nil, err
	}
	return e, nil
}
func (udao *userDAO) Delete(ctx context.Context, id string) error {
	_, err := udao.GetByID(ctx, id)
	if err != nil {
		return err
	}

	const stmt = `UPDATE user SET deleted_at = ? WHERE uuid = ?`
	if _, err := udao.DB.ExecContext(ctx, stmt, time.Now(), id); err != nil {
		return err
	}
	return nil
}

func (udao *userDAO) Count(ctx context.Context, filter string) (int, error) {
	stmt := `SELECT COUNT(*) FROM user`
	if len(filter) > 0 {
		stmt = stmt + " where " + filter
	}

	row := udao.DB.QueryRowContext(ctx, stmt)
	countVal := 0
	err := row.Scan(&countVal)
	if err != nil {
		return 0, err
	}
	return countVal, nil
}
